import { Asset, EventTarget, native, resources } from "cc";
import { NATIVE } from "cc/env";
import SparkMD5 from "spark-md5";

enum HotfixCheckError {
  Non = "",
  NoLocalMainifest = "NoLocalMainifest",
  FailToDownloadMainfest = "FailToDownloadMainfest",
  UnknownError = "UnknownError",
}

type HotfixUpdateState = "progress" | "fail" | "success";

export type {HotfixUpdateState}

enum UpdateState {
  Progress = "progress",
  Fail = "fail",
  Success = "success",
}
export { UpdateState };

export default class CCCHotfix {
  public static Event = {
    ResVersionUpdate: "ResVersionUpdate",
    Tip: "Tip",
  };

  private static _me: CCCHotfix;
  public static get me(): CCCHotfix {
    if (!this._me) {
      this._me = new CCCHotfix();
    }
    return this._me;
  }

  private _storagePath: string;
  private _e: EventTarget;
  _am: native.AssetsManager = null!;

  _checkUpdateRetryInterval: number = 3;
  _maxUpdateFailRetryTime: number = 1;
  _checkUpdateRetryMaxTime: number = 1;

  _checkUpdateRetryTime: number = 0;
  _updateFailRetryTime: number = 0;

  private _localManifestPath: string = "project";
  private _resVersion: string;

  private _checkListener: (need: boolean, error: HotfixCheckError) => void;
  private _updateListener: (
    state: HotfixUpdateState,
    progress?: number
  ) => void;

  private constructor() {
    this._e = new EventTarget();
    this._storagePath =
      (native.fileUtils ? native.fileUtils.getWritablePath() : "/") +
      "blackjack-remote-asset";

    this._checkListener = null;
    this._updateListener = null;
  }

  public get e(): EventTarget {
    return this._e;
  }

  // 是否是支持热更新的平台check
  public get working() {
    return NATIVE && (native ?? false);
  }

  public get resVersion(): string {
    return this._resVersion;
  }

  public get localManifestPath(): string {
    return this._localManifestPath;
  }

  public init(param?: {
    localManifestPath?: string;
    checkUpdateRetryMaxTime?: number;
    checkUpdateRetryInterval?: number;
    maxUpdateFailRetryTime?: number;
  }) {
    this._localManifestPath = param?.localManifestPath ?? "project";
    this._checkUpdateRetryMaxTime = param?.checkUpdateRetryMaxTime ?? 1;
    this._checkUpdateRetryInterval = param?.checkUpdateRetryInterval ?? 3;
    this._maxUpdateFailRetryTime = param?.maxUpdateFailRetryTime ?? 1;
  }

  /**
   * 检查是否需要进行更新
   * @return {Promise<{ needUpdate: boolean, error: any }>}
   *  needUpdate 是否需要进行更新;
   *  error 检查更新时的错误，如果没有错误则为 null
   */
  public async checkUpdate() {
    return new Promise<{ needUpdate: boolean; error: any }>((resolve, _) => {
      console.log("checkUpdate");
      // 非热更新平台，直接返回
      if (!this.working) {
        resolve({ needUpdate: false, error: null });
        return;
      }

      try {
        if (!this._am) {
          this._loadLocalManifest().then((manifestPath) => {
            this._initAM(manifestPath);
            this.checkUpdate().then((res) => {
              resolve(res);
            });
          }).catch((err) => {
            console.error("❌️ fail to load manifest", err);
            resolve({ needUpdate: false, error: err });
          });
        } else {
          this._internal_checkUpdate()
            .then((needUpdate) => {
              resolve({ needUpdate: needUpdate, error: null });
            })
            .catch((err) => {
              resolve({ needUpdate: false, error: err });
            });
        }
      } catch (error) {
        resolve({ needUpdate: false, error: HotfixCheckError.UnknownError });
      }
    });
  }

  async _internal_checkUpdate() {
    return new Promise<boolean>((resolve, reject) => {
      if (
        !this._am.getLocalManifest() ||
        !this._am.getLocalManifest().isLoaded()
      ) {
        this._eTip("❌️ Failed to load local manifest ...");
        console.error("❌️ Failed to load local manifest ...");
        reject("❌️ Failed to load local manifest ...");
        return;
      }

      this._checkListener = (need, err) => {
        if (need) {
          resolve(true);
          return;
        }

        if (err == HotfixCheckError.Non) {
          resolve(false);
          return;
        }

        if (err != HotfixCheckError.FailToDownloadMainfest) {
          reject(err);
          return;
        }

        if (this._checkUpdateRetryMaxTime > this._checkUpdateRetryTime) {
          setTimeout(() => {
            this._checkUpdateRetryTime++;
            console.log(
              "❗️ fail to download manifest, retry check update, retryTime: " +
                this._checkUpdateRetryTime
            );
            this._internal_checkUpdate()
              .then((bol) => resolve(bol))
              .catch((err) => reject(err));
          }, this._checkUpdateRetryInterval * 1000);
        } else {
          reject(err);
        }
      };

      this._eTip("▶️ Cheking Update...");
      console.log("♻️ Cheking Update...");
      this._am.setEventCallback(this._checkCb.bind(this));
      this._am.checkUpdate();
    });
  }

  /**
   * 实际进行更新
   * @param listener 更新进度回调：state 当前热更状态；progress 当前进度（0-100）
   */
  public doUpdate(
    listener: (state: HotfixUpdateState, progress?: number) => void
  ) {
    if (this._am) {
      this._am.setEventCallback(this._updateCb.bind(this));

      this._updateListener = listener;
      this._am.update();
    }
  }

  _initAM(manifestNativeUrl: string) {
    console.log("✅️ _storagePath", this._storagePath);
    this._am = new native.AssetsManager(
      manifestNativeUrl,
      this._storagePath,
      this._versionCompareHandle.bind(this)
    );
    this._am.setVerifyCallback(this._verifyCallback.bind(this));
  }

  private _loadLocalManifest() {
    return new Promise<string>((rso, rje) => {
      // 读取本地的 localmanifest
      resources.load(this._localManifestPath, Asset, (err, asset) => {
        if (err) {
          console.error("❌️ fail to load manifest");
          console.error(err);
          rje(err);
        } else {
          console.log(
            "✅️ success to load manifest, its nativeUrl: " + asset.nativeUrl
          );
          rso(asset.nativeUrl);
        }
      });
    });
  }

  private _versionCompareHandle(versionA: string, versionB: string) {
    this._resVersion = versionA;
    this._e.emit(CCCHotfix.Event.ResVersionUpdate, versionA);
    console.log(
      "‼️ JS Custom Version Compare: version A is " +
        versionA +
        ", version B is " +
        versionB
    );
    var vA = versionA.split(".");
    var vB = versionB.split(".");
    for (var i = 0; i < vA.length; ++i) {
      var a = parseInt(vA[i]);
      var b = parseInt(vB[i] || "0");
      if (a === b) {
        continue;
      } else {
        return a - b;
      }
    }
    if (vB.length > vA.length) {
      return -1;
    } else {
      return 0;
    }
  }

  private _verifyCallback(path: string, asset: native.ManifestAsset) {
    var compressed = asset.compressed;
    var relativePath = asset.path;
    if (compressed) {
      return true;
    } else {
      // Retrieve the correct md5 value.
      var expectedMD5 = asset.md5;
      var filemd5 = SparkMD5.ArrayBuffer.hash(
        native.fileUtils.getDataFromFile(path)
      );
      if (filemd5 == expectedMD5) {
        console.log(
          "✅️ Verification passed : " + relativePath + " (" + expectedMD5 + ")"
        );
        return true;
      }
      console.log(
        "❗️ Verification fail : " +
          relativePath +
          " (" +
          expectedMD5 +
          " vs " +
          filemd5 +
          ")"
      );
      return false;
    }
  }

  private _retry() {
    console.log("‼️ Retry failed Assets...");
    this._am.downloadFailedAssets();
  }

  private _checkCb(event: any) {
    const evtCode = event.getEventCode();
    if (evtCode == native.EventAssetsManager.UPDATE_PROGRESSION) {
      console.log("✅️ Cheking Update Progress...");
      return;
    }
    const _checkListener = this._checkListener;
    this._checkListener = null;
    console.log("✅️ CheckUpdate Callback Code: " + event.getEventCode());
    switch (event.getEventCode()) {
      case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
        this._eTip("❌️ No local manifest file found, hot update skipped.");
        console.error("❌️ message: " + event.getMessage());
        _checkListener(false, HotfixCheckError.FailToDownloadMainfest);
        break;
      case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
      case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
        this._eTip("❌️ Fail to download manifest file, hot update skipped.");
        console.error("❌️ message: " + event.getMessage());
        _checkListener(false, HotfixCheckError.FailToDownloadMainfest);
        break;
      case native.EventAssetsManager.ALREADY_UP_TO_DATE:
        this._eTip("✅️ Already up to date with the latest remote version.");
        console.log(
          "✅️ Already up to date with the latest remote version. message: " +
            event.getMessage()
        );
        _checkListener(false, HotfixCheckError.Non);
        break;
      case native.EventAssetsManager.UPDATE_FINISHED:
        this._eTip("❗️ Update finished, seems not change...");
        console.log(
          "❗️ Update finished, seems not change... message: " +
            event.getMessage()
        );
        _checkListener(false, HotfixCheckError.Non);
        break;
      case native.EventAssetsManager.NEW_VERSION_FOUND:
        this._eTip(
          "❇️ New version found, please try to update. (" +
            Math.ceil(this._am.getTotalBytes() / 1024) +
            "kb)"
        );
        console.log(
          "❇️ New version found, please try to update. message: " +
            event.getMessage()
        );
        _checkListener(true, HotfixCheckError.Non);
        break;
      default:
        return;
    }
  }

  private _updateCb(event: native.EventAssetsManager) {
    var needRestart = false;
    var failed = false;
    var retry = false;
    switch (event.getEventCode()) {
      case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
        this._eTip("❌️ No local manifest file found, hot update skipped.");
        console.error("❌️ message: " + event.getMessage());
        failed = true;
        break;
      case native.EventAssetsManager.UPDATE_PROGRESSION:
        var msg = event.getMessage();
        if (msg) {
          this._eTip("✅️ Updated file: " + msg);
          console.log("✅️ Updated file: " + msg);
          this._updateListener("progress", event.getPercentByFile());
        }
        break;
      case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
      case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
        this._eTip("❌️ Fail to download manifest file, hot update skipped.");
        console.error("❌️ message: " + event.getMessage());
        failed = true;
        break;
      case native.EventAssetsManager.ALREADY_UP_TO_DATE:
        this._eTip("✅️ Already up to date with the latest remote version.");
        console.log(
          "✅️ updateCb Already up to date with the latest remote version. message: " +
            event.getMessage()
        );
        failed = true;
        break;
      case native.EventAssetsManager.UPDATE_FINISHED:
        this._eTip("✅️ Update finished. " + event.getMessage());
        console.log(
          "✅️ updateCb Update finished. message: " + event.getMessage()
        );
        needRestart = true;
        break;
      case native.EventAssetsManager.UPDATE_FAILED:
        this._eTip("❌️ Update failed. " + event.getMessage());
        console.error("❌️ Update failed message: " + event.getMessage());
        retry = true;
        break;
      case native.EventAssetsManager.ERROR_UPDATING:
        this._eTip(
          "❌️ Asset update error: " +
            event.getAssetId() +
            ", " +
            event.getMessage()
        );
        console.error(
          "❌️ Asset update error message: " +
            event.getMessage() +
            " assetId: " +
            event.getAssetId()
        );

        break;
      case native.EventAssetsManager.ERROR_DECOMPRESS:
        this._eTip("❌️ Asset decompress error: " + event.getMessage());
        console.error(
          "❌️ Asset decompress error message: " + event.getMessage()
        );
        break;
      default:
        break;
    }

    if (retry) {
      if (this._updateFailRetryTime < this._maxUpdateFailRetryTime) {
        this._updateFailRetryTime++;
        this._retry();
      } else {
        failed = true;
      }
    }

    if (failed) {
      this._am.setEventCallback(null!);
      this._updateListener("fail");
      this._updateListener = null;
    }

    if (needRestart) {
      this._am.setEventCallback(null!);
      // Prepend the manifest's search path
      var searchPaths = native.fileUtils.getSearchPaths();
      var newPaths = this._am.getLocalManifest().getSearchPaths();
      console.log(JSON.stringify(newPaths));
      Array.prototype.unshift.apply(searchPaths, newPaths);
      // This value will be retrieved and appended to the default search path during game startup,
      // please refer to samples/js-tests/main.js for detailed usage.
      // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
      localStorage.setItem("HotUpdateSearchPaths", JSON.stringify(searchPaths));
      native.fileUtils.setSearchPaths(searchPaths);
      this._updateListener("success");
      this._updateListener = null;
    }
  }

  private _eTip(tip: string) {
    this.e.emit(CCCHotfix.Event.Tip, tip);
  }
}
